/*******************************************************************************
 * This file is part of Crunch Graphics.
 *
 * Crunch Graphics is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Graphics is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Graphics.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.graphics.gpu;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

/** Encapsulates a program on the GPU.
 */
public class GpuProgram extends ReferenceCountedGpuObject {
	/** Description of program settings.
	 */
	public static class ProgramDescription {
		public String[] vertexShaderSource;     /**< Source code for the vertex shader.*/
		public String[] geometryShaderSource;   /**< Source code for the geometry shader.*/
		public String[] fragmentShaderSource;   /**< Source code for the fragment shader.*/

		public ProgramDescription() {           /**< Sets default values.*/
			vertexShaderSource = null;
			geometryShaderSource = null;
			fragmentShaderSource = null;
		}
	}

	/** Description of a uniform in a program.
	 */
	public static class UniformDescription {
		public String name;     /**< The name of the uniform.*/
		public int type;        /**< The GLSL type.*/
		public int arrayCount;  /**< The size if an array (1 otherwise).*/
	}

	/** Description of an attribute in a program.
	 */
	public static class AttributeDescription {
		public String name;     /**< The name of the attribute.*/
		public int type;        /**< The GLSL type.*/
		public int arrayCount;  /**< The size if an array (1 otherwise).*/
	}

	/** Returns the vertex shader ID.
	 */
	public int getVertexShaderId() {
		return shaderIds[ShaderType.VERTEX_SHADER.ordinal()];
	}

	/** Returns the geometry shader ID.
	 */
	public int getGeometryShaderId() {
		return shaderIds[ShaderType.GEOMETRY_SHADER.ordinal()];
	}

	/** Returns the fragment shader ID.
	 */
	public int getFragmentShaderId() {
		return shaderIds[ShaderType.FRAGMENT_SHADER.ordinal()];
	}

	/** Returns the program ID.
	 */
	public int getProgramId() {
		return programId;
	}

	/** Returns the number of uniforms.
	 */
	public int getUniformCount() {
		return uniformData.length;
	}

	/** Returns the description of the uniform with the given index.
	 */
	public UniformDescription getUniformDescription(int index) {
		if (index >= uniformData.length) {
			throw new IllegalArgumentException("Invalid uniform index");
		}
		UniformDescription ud = new UniformDescription();
		ud.name = uniformData[index].desc.name;
		ud.type = uniformData[index].desc.type;
		ud.arrayCount = uniformData[index].desc.arrayCount;
		return ud;
	}

	/** Returns the location of the uniform with the given index, or -1 if it does not exist.
	 */
	public int getUniformLocation(int index) {
		return getUniformLocation(index, 0);
	}

	/** Returns the location of the uniform with the given index, or -1 if it does not exist.
	 */
	public int getUniformLocation(int index, int arrayOffset) {
		if (index >= uniformData.length) {
			return -1;
		}
		if (arrayOffset >= uniformData[index].desc.arrayCount) {
			return -1;
		}
		return uniformLocations.get(uniformData[index].locationIndex + arrayOffset);
	}

	/** Returns the location of the uniform with the given name, or -1 if it does not exist.
	 */
	public int getUniformLocation(String name) {
		return getUniformLocation(name, 0);
	}

	/** Returns the location of the uniform with the given name, or -1 if it does not exist.
	 */
	public int getUniformLocation(String name, int arrayOffset) {
		int index = getUniformIndex(name);
		if (index < 0) {
			return -1;
		}
		return getUniformLocation(index, arrayOffset);
	}

	/** Returns the index of the uniform with the given name, or -1 if it does not exist.
	 */
	public int getUniformIndex(String name) {
		Integer index = uniformMap.get(name);
		return (index == null) ? -1 : index;
	}

	/** Sets the value of the uniform with the given index.
	 */
	public boolean setUniform(int index, int type, int[] data) {
		return setUniform(index, type, data, 0, 1);
	}

	/** Sets the value of the uniform with the given index.
	 */
	public boolean setUniform(int index, int type, int[] data, int arrayOffset) {
		return setUniform(index, type, data, arrayOffset, 1);
	}

	/** Sets the value of the uniform with the given index.
	 *
	 *  This function should be used for integer types.
	 *  The provided data is interpreted based on the uniform type.
	 *  The return value indicates whether the uniform was successfully set.
	 */
	public boolean setUniform(int index, int type, int[] data, int arrayOffset, int arrayCount) {
		if (index >= uniformData.length) {
			return false;
		}
		UniformData ud = uniformData[index];
		if (ud.desc.type != type) {
			return false;
		}
		if (data.length < arrayCount * ud.typeDescription.elements) {
			return false;
		}
		if (arrayOffset >= ud.desc.arrayCount) {
			// we don't return errors on array size problems - this is because GLSL sometimes optimizes away some entries
			return true;
		}
		// adjust so we don't go past the end
		arrayCount = Math.min(arrayCount, ud.desc.arrayCount - arrayOffset);

		int cacheOffset = ud.cacheOffset + ud.typeDescription.elements * arrayOffset;
		int entries = arrayCount * ud.typeDescription.elements;
		boolean upload = false;
		// compare entries in the cache
		switch (ud.typeDescription.baseType) {
			case GL.GL_FLOAT:
				return false;
			case GL3.GL_INT:
			case GL3.GL_BOOL:
			case GL.GL_UNSIGNED_INT:
				for (int i = 0; i < entries; ++i) {
					if (uniformCache[cacheOffset + i] != data[i]) {
						upload = true;
						break;
					}
				}
				break;
		}

		if (!upload) {
			// nothing to upload - success
			return true;
		}

		bind();

		// upload and update cache
		int loc = uniformLocations.get(ud.locationIndex + arrayOffset);
		switch (ud.desc.type) {
			case GL.GL_FLOAT:
			case GL3.GL_FLOAT_VEC2:
			case GL3.GL_FLOAT_VEC3:
			case GL3.GL_FLOAT_VEC4:
				return false;
			case GL3.GL_INT:
			case GL3.GL_BOOL:
				getGpuState().getGl().glUniform1iv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_INT_VEC2:
			case GL3.GL_BOOL_VEC2:
				getGpuState().getGl().glUniform2iv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_INT_VEC3:
			case GL3.GL_BOOL_VEC3:
				getGpuState().getGl().glUniform3iv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_INT_VEC4:
			case GL3.GL_BOOL_VEC4:
				getGpuState().getGl().glUniform4iv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_UNSIGNED_INT:
				getGpuState().getGl().glUniform1uiv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_UNSIGNED_INT_VEC2:
				getGpuState().getGl().glUniform2uiv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_UNSIGNED_INT_VEC3:
				getGpuState().getGl().glUniform3uiv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_UNSIGNED_INT_VEC4:
				getGpuState().getGl().glUniform4uiv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_FLOAT_MAT2:
			case GL3.GL_FLOAT_MAT3:
			case GL3.GL_FLOAT_MAT4:
			case GL3.GL_FLOAT_MAT2x3:
			case GL3.GL_FLOAT_MAT2x4:
			case GL3.GL_FLOAT_MAT3x2:
			case GL3.GL_FLOAT_MAT3x4:
			case GL3.GL_FLOAT_MAT4x2:
			case GL3.GL_FLOAT_MAT4x3:
				return false;
			case GL3.GL_SAMPLER_1D:
			case GL3.GL_SAMPLER_2D:
			case GL3.GL_SAMPLER_3D:
			case GL3.GL_SAMPLER_CUBE:
			case GL3.GL_SAMPLER_1D_SHADOW:
			case GL3.GL_SAMPLER_2D_SHADOW:
			case GL3.GL_SAMPLER_1D_ARRAY:
			case GL3.GL_SAMPLER_2D_ARRAY:
			case GL3.GL_SAMPLER_1D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_SAMPLER_CUBE_SHADOW:
			case GL3.GL_SAMPLER_BUFFER:
			case GL3.GL_SAMPLER_2D_RECT:
			case GL3.GL_SAMPLER_2D_RECT_SHADOW:
			case GL3.GL_INT_SAMPLER_1D:
			case GL3.GL_INT_SAMPLER_2D:
			case GL3.GL_INT_SAMPLER_3D:
			case GL3.GL_INT_SAMPLER_CUBE:
			case GL3.GL_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_INT_SAMPLER_BUFFER:
			case GL3.GL_INT_SAMPLER_2D_RECT:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_3D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_CUBE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_BUFFER:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_RECT:
				getGpuState().getGl().glUniform1iv(loc, arrayCount, data, 0);
				break;
		}

		System.arraycopy(data, 0, uniformCache, cacheOffset, entries);

		return true;
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, int[] data) {
		return setUniform(name, type, data, 0, 1);
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, int[] data, int arrayOffset) {
		return setUniform(name, type, data, arrayOffset, 1);
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, int[] data, int arrayOffset, int arrayCount) {
		int index = getUniformIndex(name);
		if (index < 0) {
			return false;
		}
		setUniform(index, type, data, arrayOffset, arrayCount);
		return true;
	}

	/** Sets the value of the uniform with the given index.
	 */
	public boolean setUniform(int index, int type, float[] data) {
		return setUniform(index, type, data, 0, 1);
	}

	/** Sets the value of the uniform with the given index.
	 */
	public boolean setUniform(int index, int type, float[] data, int arrayOffset) {
		return setUniform(index, type, data, arrayOffset, 1);
	}

	/** Sets the value of the uniform with the given index.
	 *
	 *  This function should be used for float types.
	 *  The provided data is interpreted based on the uniform type.
	 *  The return value indicates whether the uniform was successfully set.
	 */
	public boolean setUniform(int index, int type, float[] data, int arrayOffset, int arrayCount) {
		if (index >= uniformData.length) {
			return false;
		}
		UniformData ud = uniformData[index];
		if (ud.desc.type != type) {
			return false;
		}
		if (data.length < arrayCount * ud.typeDescription.elements) {
			return false;
		}
		if (arrayOffset >= ud.desc.arrayCount) {
			// we don't return errors on array size problems - this is because GLSL sometimes optimizes away some entries
			return true;
		}
		// adjust so we don't go past the end
		arrayCount = Math.min(arrayCount, ud.desc.arrayCount - arrayOffset);

		int cacheOffset = ud.cacheOffset + ud.typeDescription.elements * arrayOffset;
		int entries = arrayCount * ud.typeDescription.elements;
		boolean upload = false;
		// compare entries in the cache
		switch (ud.typeDescription.baseType) {
			case GL.GL_FLOAT:
				for (int i = 0; i < entries; ++i) {
					if (Float.intBitsToFloat(uniformCache[cacheOffset + i]) != data[i]) {
						upload = true;
						break;
					}
				}
				break;
			case GL3.GL_INT:
			case GL3.GL_BOOL:
			case GL.GL_UNSIGNED_INT:
				return false;
		}

		if (!upload) {
			// nothing to upload - success
			return true;
		}

		bind();

		// upload and update cache
		int loc = uniformLocations.get(ud.locationIndex + arrayOffset);
		switch (ud.desc.type) {
			case GL.GL_FLOAT:
				getGpuState().getGl().glUniform1fv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_FLOAT_VEC2:
				getGpuState().getGl().glUniform2fv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_FLOAT_VEC3:
				getGpuState().getGl().glUniform3fv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_FLOAT_VEC4:
				getGpuState().getGl().glUniform4fv(loc, arrayCount, data, 0);
				break;
			case GL3.GL_INT:
			case GL3.GL_BOOL:
			case GL3.GL_INT_VEC2:
			case GL3.GL_BOOL_VEC2:
			case GL3.GL_INT_VEC3:
			case GL3.GL_BOOL_VEC3:
			case GL3.GL_INT_VEC4:
			case GL3.GL_BOOL_VEC4:
			case GL3.GL_UNSIGNED_INT:
			case GL3.GL_UNSIGNED_INT_VEC2:
			case GL3.GL_UNSIGNED_INT_VEC3:
			case GL3.GL_UNSIGNED_INT_VEC4:
				return false;
			case GL3.GL_FLOAT_MAT2:
				getGpuState().getGl().glUniformMatrix2fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT3:
				getGpuState().getGl().glUniformMatrix3fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT4:
				getGpuState().getGl().glUniformMatrix4fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT2x3:
				getGpuState().getGl().glUniformMatrix2x3fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT2x4:
				getGpuState().getGl().glUniformMatrix2x4fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT3x2:
				getGpuState().getGl().glUniformMatrix3x2fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT3x4:
				getGpuState().getGl().glUniformMatrix3x4fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT4x2:
				getGpuState().getGl().glUniformMatrix4x2fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_FLOAT_MAT4x3:
				getGpuState().getGl().glUniformMatrix4x3fv(loc, arrayCount, false, data, 0);
				break;
			case GL3.GL_SAMPLER_1D:
			case GL3.GL_SAMPLER_2D:
			case GL3.GL_SAMPLER_3D:
			case GL3.GL_SAMPLER_CUBE:
			case GL3.GL_SAMPLER_1D_SHADOW:
			case GL3.GL_SAMPLER_2D_SHADOW:
			case GL3.GL_SAMPLER_1D_ARRAY:
			case GL3.GL_SAMPLER_2D_ARRAY:
			case GL3.GL_SAMPLER_1D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_SAMPLER_CUBE_SHADOW:
			case GL3.GL_SAMPLER_BUFFER:
			case GL3.GL_SAMPLER_2D_RECT:
			case GL3.GL_SAMPLER_2D_RECT_SHADOW:
			case GL3.GL_INT_SAMPLER_1D:
			case GL3.GL_INT_SAMPLER_2D:
			case GL3.GL_INT_SAMPLER_3D:
			case GL3.GL_INT_SAMPLER_CUBE:
			case GL3.GL_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_INT_SAMPLER_BUFFER:
			case GL3.GL_INT_SAMPLER_2D_RECT:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_3D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_CUBE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_BUFFER:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_RECT:
				return false;
		}

		for (int i = 0; i < entries; ++i) {
			uniformCache[cacheOffset + i] = Float.floatToIntBits(data[i]);
		}

		return true;
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, float[] data) {
		return setUniform(name, type, data, 0, 1);
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, float[] data, int arrayOffset) {
		return setUniform(name, type, data, arrayOffset, 1);
	}

	/** Sets the value of the uniform with the given name.
	 */
	public boolean setUniform(String name, int type, float[] data, int arrayOffset, int arrayCount) {
		int index = getUniformIndex(name);
		if (index < 0) {
			return false;
		}
		setUniform(index, type, data, arrayOffset, arrayCount);
		return true;
	}

	/** Returns the number of attributes.
	 */
	public int getAttributeCount() {
		return attributeData.length;
	}

	/** Returns the description of the attribute with the given index.
	 */
	public AttributeDescription getAttributeDescription(int index) {
		if (index >= attributeData.length) {
			throw new IllegalArgumentException("Invalid attribute index");
		}
		AttributeDescription ad = new AttributeDescription();
		ad.name = attributeData[index].desc.name;
		ad.type = attributeData[index].desc.type;
		ad.arrayCount = attributeData[index].desc.arrayCount;
		return ad;
	}

	/** Returns the location of the attribute with the given index, or -1 if it does not exist.
	 */
	public int getAttributeLocation(int index) {
		if (index >= attributeData.length) {
			return -1;
		}
		return attributeData[index].location;
	}

	/** Returns the location of the attribute with the given name, or -1 if it does not exist.
	 */
	public int getAttributeLocation(String name) {
		int index = getAttributeIndex(name);
		if (index < 0) {
			return -1;
		}
		return getAttributeLocation(index);
	}

	/** Returns the index of the attribute with the given name, or -1 if it does not exist.
	 */
	public int getAttributeIndex(String name) {
		Integer index = attributeMap.get(name);
		return (index == null) ? -1 : index;
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data) {
		return setAttribute(index, size, type, normalized, integer, stride, data, 0, 1);
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset) {
		return setAttribute(index, size, type, normalized, integer, stride, data, arrayOffset, 1);
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset, int arrayCount) {
		if (index >= attributeData.length) {
			return false;
		}
		AttributeData ad = attributeData[index];
		if (ad.typeDescription.elements != size) {
			return false;
		}
		if (arrayOffset >= ad.desc.arrayCount) {
			// don't make array bound issues an error
			return true;
		}
		arrayCount = Math.min(arrayCount, ad.desc.arrayCount - arrayOffset);
		int entrySize = getOpenGLAttributeTypeSize( type, size );
		boolean success = true;
		for (int i = 0; i < arrayCount; ++i) {
			GpuAttribute attrib = getGpuState().getAttribute(ad.location + arrayOffset + i);
			attrib.setEnabled(true);
			success &= attrib.setPointer(size, type, normalized, integer, stride, data + i*entrySize);
		}
		return success;
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data) {
		return setAttribute(name, size, type, normalized, integer, stride, data, 0, 1);
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset) {
		return setAttribute(name, size, type, normalized, integer, stride, data, arrayOffset, 1);
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset, int arrayCount) {
		int index = getAttributeIndex(name);
		if (index < 0) {
			return false;
		}
		return setAttribute(index, size, type, normalized, integer, stride, data, arrayOffset, arrayCount);
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data) {
		return setAttribute(index, buffer, size, type, normalized, integer, stride, data, 0, 1);
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset) {
		return setAttribute(index, buffer, size, type, normalized, integer, stride, data, arrayOffset, 1);
	}

	/** Enables and sets the value of the attribute with the given name.
	 */
	public boolean setAttribute(int index, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset, int arrayCount) {
		if (index >= attributeData.length) {
			return false;
		}
		AttributeData ad = attributeData[index];
		if (ad.typeDescription.elements != size) {
			return false;
		}
		if (arrayOffset >= ad.desc.arrayCount) {
			// don't make array bound issues an error
			return true;
		}
		arrayCount = Math.min(arrayCount, ad.desc.arrayCount - arrayOffset);
		int entrySize = getOpenGLAttributeTypeSize( type, size );
		boolean success = true;
		for (int i = 0; i < arrayCount; ++i) {
			GpuAttribute attrib = getGpuState().getAttribute(ad.location + arrayOffset + i);
			attrib.setEnabled(true);
			success &= attrib.setPointer(buffer, size, type, normalized, integer, stride, data + i*entrySize);
		}
		return success;
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data) {
		return setAttribute(name, buffer, size, type, normalized, integer, stride, data, 0, 1);
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset) {
		return setAttribute(name, buffer, size, type, normalized, integer, stride, data, arrayOffset, 1);
	}

	/** Enables and sets the value of the uniform with the given name.
	 */
	public boolean setAttribute(String name, GpuBuffer buffer, int size, int type, boolean normalized, boolean integer, int stride,
	                            long data, int arrayOffset, int arrayCount) {
		int index = getAttributeIndex(name);
		if (index < 0) {
			return false;
		}
		return setAttribute(index, buffer, size, type, normalized, integer, stride, data, arrayOffset, arrayCount);
	}

	/** Binds the program.
	 */
	public void bind() {
		getGpuState().bindProgram(this);
	}

	/** Describes a GLSL uniform type.
	 */
	public static class GlslUniformTypeDescription {
		public int baseType;    /**< The base type.*/
		public int elements;    /**< The number of elements.*/

		/** Constructor assigning values.
		 */
		public GlslUniformTypeDescription(int base, int elem) {
			baseType = base;
			elements = elem;
		}
	}

	/** Returns a type description of a GLSL uniform type.
	 */
	public static GlslUniformTypeDescription getGlslUniformTypeDescription(int type) {
		switch (type) {
			case GL.GL_FLOAT:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 1);
			case GL3.GL_FLOAT_VEC2:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 2);
			case GL3.GL_FLOAT_VEC3:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 3);
			case GL3.GL_FLOAT_VEC4:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 4);
			case GL3.GL_INT:
				return new GlslUniformTypeDescription(GL3.GL_INT, 1);
			case GL3.GL_INT_VEC2:
				return new GlslUniformTypeDescription(GL3.GL_INT, 2);
			case GL3.GL_INT_VEC3:
				return new GlslUniformTypeDescription(GL3.GL_INT, 3);
			case GL3.GL_INT_VEC4:
				return new GlslUniformTypeDescription(GL3.GL_INT, 4);
			case GL.GL_UNSIGNED_INT:
				return new GlslUniformTypeDescription(GL.GL_UNSIGNED_INT, 1);
			case GL3.GL_UNSIGNED_INT_VEC2:
				return new GlslUniformTypeDescription(GL.GL_UNSIGNED_INT, 2);
			case GL3.GL_UNSIGNED_INT_VEC3:
				return new GlslUniformTypeDescription(GL.GL_UNSIGNED_INT, 3);
			case GL3.GL_UNSIGNED_INT_VEC4:
				return new GlslUniformTypeDescription(GL.GL_UNSIGNED_INT, 4);
			case GL3.GL_BOOL:
				return new GlslUniformTypeDescription(GL3.GL_BOOL, 1);
			case GL3.GL_BOOL_VEC2:
				return new GlslUniformTypeDescription(GL3.GL_BOOL, 2);
			case GL3.GL_BOOL_VEC3:
				return new GlslUniformTypeDescription(GL3.GL_BOOL, 3);
			case GL3.GL_BOOL_VEC4:
				return new GlslUniformTypeDescription(GL3.GL_BOOL, 4);
			case GL3.GL_FLOAT_MAT2:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 4);
			case GL3.GL_FLOAT_MAT3:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 9);
			case GL3.GL_FLOAT_MAT4:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 16);
			case GL3.GL_FLOAT_MAT2x3:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 6);
			case GL3.GL_FLOAT_MAT2x4:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 8);
			case GL3.GL_FLOAT_MAT3x2:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 6);
			case GL3.GL_FLOAT_MAT3x4:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 12);
			case GL3.GL_FLOAT_MAT4x2:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 8);
			case GL3.GL_FLOAT_MAT4x3:
				return new GlslUniformTypeDescription(GL.GL_FLOAT, 16);
			case GL3.GL_SAMPLER_1D:
			case GL3.GL_SAMPLER_2D:
			case GL3.GL_SAMPLER_3D:
			case GL3.GL_SAMPLER_CUBE:
			case GL3.GL_SAMPLER_1D_SHADOW:
			case GL3.GL_SAMPLER_2D_SHADOW:
			case GL3.GL_SAMPLER_1D_ARRAY:
			case GL3.GL_SAMPLER_2D_ARRAY:
			case GL3.GL_SAMPLER_1D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_ARRAY_SHADOW:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_SAMPLER_CUBE_SHADOW:
			case GL3.GL_SAMPLER_BUFFER:
			case GL3.GL_SAMPLER_2D_RECT:
			case GL3.GL_SAMPLER_2D_RECT_SHADOW:
			case GL3.GL_INT_SAMPLER_1D:
			case GL3.GL_INT_SAMPLER_2D:
			case GL3.GL_INT_SAMPLER_3D:
			case GL3.GL_INT_SAMPLER_CUBE:
			case GL3.GL_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_INT_SAMPLER_BUFFER:
			case GL3.GL_INT_SAMPLER_2D_RECT:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_3D:
			case GL3.GL_UNSIGNED_INT_SAMPLER_CUBE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
			case GL3.GL_UNSIGNED_INT_SAMPLER_BUFFER:
			case GL3.GL_UNSIGNED_INT_SAMPLER_2D_RECT:
				// all samplers are treated as ints
				return new GlslUniformTypeDescription(GL3.GL_INT, 1);
			default:
				throw new IllegalArgumentException("Invalid GLSL uniform type");
		}
	}

	/** Describes a GLSL attribute type.
	 */
	public static class GlslAttributeTypeDescription {
		public int baseType;    /**< The base type.*/
		public int elements;    /**< The number of elements.*/
		public int locations;   /**< The number of locations used (e.g. GL_FLOAT_MAT4 uses 4).*/

		/** Constructor assigning values.
		 */
		public GlslAttributeTypeDescription(int base, int elem, int locs) {
			baseType = base;
			elements = elem;
			locations = locs;
		}
	}

	/** Returns a type description of a GLSL attribute type.
	 */
	public static GlslAttributeTypeDescription getGlslAttributeTypeDescription(int type) {
		switch (type) {
			case GL.GL_FLOAT:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 1, 1);
			case GL3.GL_FLOAT_VEC2:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 2, 1);
			case GL3.GL_FLOAT_VEC3:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 3, 1);
			case GL3.GL_FLOAT_VEC4:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 4, 1);
			case GL3.GL_FLOAT_MAT2:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 2, 2);
			case GL3.GL_FLOAT_MAT3:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 3, 3);
			case GL3.GL_FLOAT_MAT4:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 4, 4);
			case GL3.GL_FLOAT_MAT2x3:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 2, 3);
			case GL3.GL_FLOAT_MAT2x4:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 2, 4);
			case GL3.GL_FLOAT_MAT3x2:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 3, 2);
			case GL3.GL_FLOAT_MAT3x4:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 3, 4);
			case GL3.GL_FLOAT_MAT4x2:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 4, 2);
			case GL3.GL_FLOAT_MAT4x3:
				return new GlslAttributeTypeDescription(GL.GL_FLOAT, 4, 3);
			case GL3.GL_INT:
				return new GlslAttributeTypeDescription(GL3.GL_INT, 1, 1);
			case GL3.GL_INT_VEC2:
				return new GlslAttributeTypeDescription(GL3.GL_INT, 2, 1);
			case GL3.GL_INT_VEC3:
				return new GlslAttributeTypeDescription(GL3.GL_INT, 3, 1);
			case GL3.GL_INT_VEC4:
				return new GlslAttributeTypeDescription(GL3.GL_INT, 4, 1);
			case GL.GL_UNSIGNED_INT:
				return new GlslAttributeTypeDescription(GL.GL_UNSIGNED_INT, 1, 1);
			case GL3.GL_UNSIGNED_INT_VEC2:
				return new GlslAttributeTypeDescription(GL.GL_UNSIGNED_INT, 2, 1);
			case GL3.GL_UNSIGNED_INT_VEC3:
				return new GlslAttributeTypeDescription(GL.GL_UNSIGNED_INT, 3, 1);
			case GL3.GL_UNSIGNED_INT_VEC4:
				return new GlslAttributeTypeDescription(GL.GL_UNSIGNED_INT, 4, 1);
			default:
				throw new IllegalArgumentException( "Invalid GLSL attribute type" );
		}
	}

	/** Returns the size in bytes of a GLSL attribute base type (e.g. GL_FLOAT, but not GL_FLOAT_VEC2).
	 */
	public static int getOpenGLAttributeTypeSize(int type, int elements) {
		switch (type) {
			case GL.GL_BYTE:
				return elements;
			case GL.GL_UNSIGNED_BYTE:
				return elements;
			case GL.GL_SHORT:
				return 2 * elements;
			case GL.GL_UNSIGNED_SHORT:
				return 2 * elements;
			case GL3.GL_INT:
				return 4 * elements;
			case GL.GL_UNSIGNED_INT:
				return 4 * elements;
			case GL.GL_HALF_FLOAT:
				return 2 * elements;
			case GL.GL_FLOAT:
				return 4 * elements;
			case GL3.GL_DOUBLE:
				return 8 * elements;
			case GL3.GL_INT_2_10_10_10_REV:
				return 4;
			case GL3.GL_UNSIGNED_INT_2_10_10_10_REV:
				return 4;
			default:
				throw new IllegalArgumentException("Invalid OpenGL attribute type");
		}
	}

	GpuProgram(GpuState state, ProgramDescription desc)
			throws GpuException {
		super(state);
		uniformLocations = new ArrayList<Integer>();
		uniformMap = new HashMap<String, Integer>();
		attributeMap = new HashMap<String, Integer>();
		Arrays.fill(shaderIds, 0);
		programId = 0;

		final String[] SHADER_NAMES = {
				"vertex", "geometry", "fragment"
		};
		final int[] SHADER_TYPES = {
				GL3.GL_VERTEX_SHADER, GL3.GL_GEOMETRY_SHADER, GL3.GL_FRAGMENT_SHADER
		};
		String[][] shaderSources = {
				desc.vertexShaderSource, desc.geometryShaderSource, desc.fragmentShaderSource
		};

		try {
			for (int i = 0; i < ShaderType.values().length; ++i) {
				if (shaderSources[i] == null) {
					continue;
				}

				shaderIds[i] = getGpuState().getGl().glCreateShader(SHADER_TYPES[i]);
				getGpuState().getGl().glShaderSource(shaderIds[i], shaderSources[i].length, shaderSources[i], null);
				getGpuState().getGl().glCompileShader(shaderIds[i]);

				// get compile results
				int[] logLength = new int[1];
				getGpuState().getGl().glGetShaderiv(shaderIds[i], GL3.GL_INFO_LOG_LENGTH, logLength, 0);
				if (logLength[0] > 0) {
					byte[] logContent = new byte[logLength[0]];
					getGpuState().getGl().glGetShaderInfoLog(shaderIds[i], logLength[0], null, 0, logContent, 0);
					try {
						int length = 0;
						while (length < logContent.length && logContent[length] != 0) {
							++length;
						}
						String logString = new String(logContent, 0, length, "UTF-8");
						System.out.println(logString);
					} catch (UnsupportedEncodingException e) {
						System.out.println("Unsupported info log encoding");
					}
				}

				int[] compileStatus = new int[1];
				getGpuState().getGl().glGetShaderiv(shaderIds[i], GL3.GL_COMPILE_STATUS, compileStatus, 0);
				if (compileStatus[0] == GL.GL_FALSE) {
					throw new GpuException("Failed to compile " + SHADER_NAMES[i] + " shader");
				}
			}

			programId = getGpuState().getGl().glCreateProgram();
			for (int i = 0; i < ShaderType.values().length; ++i) {
				if (shaderIds[i] != 0) {
					getGpuState().getGl().glAttachShader(programId, shaderIds[i]);
				}
			}
			getGpuState().getGl().glLinkProgram(programId);

			// get link results
			int[] logLength = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_INFO_LOG_LENGTH, logLength, 0);
			if (logLength[0] > 0) {
				byte[] logContent = new byte[logLength[0]];
				getGpuState().getGl().glGetProgramInfoLog(programId, logLength[0], null, 0, logContent, 0);
				try {
					int length = 0;
					while (length < logContent.length && logContent[length] != 0) {
						++length;
					}
					String logString = new String(logContent, 0, length, "UTF-8");
					System.out.println(logString);
				} catch (UnsupportedEncodingException e) {
					System.out.println("Unsupported info log encoding");
				}
			}

			int[] linkStatus = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_LINK_STATUS, linkStatus, 0);
			if (linkStatus[0] != GL.GL_TRUE) {
				throw new GpuException("Failed to link program");
			}

			// set up the uniforms

			int[] uniformCount = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_ACTIVE_UNIFORMS, uniformCount, 0);
			int[] maxUniformLength = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_ACTIVE_UNIFORM_MAX_LENGTH, maxUniformLength, 0);
			byte[] uniformNameBuffer = new byte[maxUniformLength[0]];

			uniformData = new UniformData[uniformCount[0]];
			uniformLocations.ensureCapacity(uniformCount[0]); // arrays will need more
			int requiredCacheSize = 0;
			for (int i = 0; i < uniformCount[0]; ++i) {
				int[] arrayCount = new int[1];
				int[] type = new int[1];
				getGpuState().getGl().glGetActiveUniform(programId, i, maxUniformLength[0], null, 0, arrayCount, 0, type, 0, uniformNameBuffer, 0);
				int nameLength = 0;
				while (nameLength < uniformNameBuffer.length && uniformNameBuffer[nameLength] != 0) {
					++nameLength;
				}
				// check for built in uniform names (they start with "gl_")
				// we want to ignore these
				if (nameLength >= 3 &&
						uniformNameBuffer[0] == 'g' &&
						uniformNameBuffer[1] == 'l' &&
						uniformNameBuffer[2] == '_')
					continue;

				// remove array notation - arrays might end in "[0]"
				if (nameLength > 0 && uniformNameBuffer[nameLength-1] == ']') {
					while (nameLength > 0) {
						if (uniformNameBuffer[nameLength-1] == '[') {
							--nameLength;
							break;
						} else {
							--nameLength;
						}
					}
				}
				UniformData ud = new UniformData();
				ud.desc.name = new String(uniformNameBuffer, 0, nameLength, "UTF-8");
				ud.desc.type = type[0];
				ud.desc.arrayCount = arrayCount[0];
				ud.typeDescription = getGlslUniformTypeDescription(type[0]);
				ud.locationIndex = uniformLocations.size();
				if (ud.desc.arrayCount == 1) {
					uniformLocations.add(getGpuState().getGl().glGetUniformLocation(programId, ud.desc.name));
				} else {
					// query the location of each element
					// use array [t] notation
					for (int t = 0; t < ud.desc.arrayCount; ++t) {
						uniformLocations.add(getGpuState().getGl().glGetUniformLocation(programId, ud.desc.name + '[' + t + ']'));
					}
				}

				// add to required cache size
				ud.cacheOffset = requiredCacheSize;
				requiredCacheSize += ud.typeDescription.elements * ud.desc.arrayCount;

				// add uniform
				uniformMap.put(ud.desc.name, i);
				uniformData[i] = ud;
			}

			// set up initial values in the cache
			uniformCache = new int[requiredCacheSize];
			for (int i = 0; i < uniformData.length; ++i) {
				UniformData ud = uniformData[i];
				for (int t = 0; t < ud.desc.arrayCount; ++t) {
					int loc = uniformLocations.get(ud.locationIndex + t);
					int cache = ud.cacheOffset + t*ud.typeDescription.elements;
					switch (ud.typeDescription.baseType) {
						case GL.GL_FLOAT:
						{
							float[] buf = new float[ud.typeDescription.elements];
							getGpuState().getGl().glGetUniformfv(programId, loc, buf, 0);
							for (int c = 0; c < buf.length; ++c) {
								uniformCache[cache + c] = Float.floatToIntBits(buf[c]);
							}
							break;
						}
						case GL3.GL_INT:
						case GL3.GL_BOOL:
						{
							int[] buf = new int[ud.typeDescription.elements];
							getGpuState().getGl().glGetUniformiv(programId, loc, buf, 0);
							System.arraycopy(buf, 0, uniformCache, cache, buf.length);
							break;
						}
						case GL.GL_UNSIGNED_INT:
						{
							int[] buf = new int[ud.typeDescription.elements];
							getGpuState().getGl().glGetUniformuiv(programId, loc, buf, 0);
							System.arraycopy(buf, 0, uniformCache, cache, buf.length);
							break;
						}
					}
				}
			}

			// set up attributes

			int[] attributeCount = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_ACTIVE_ATTRIBUTES, attributeCount, 0);
			int[] attributeMaxLength = new int[1];
			getGpuState().getGl().glGetProgramiv(programId, GL3.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, attributeMaxLength, 0);
			byte[] attributeNameBuffer = new byte[attributeMaxLength[0]];

			attributeData = new AttributeData[attributeCount[0]];
			for (int i = 0; i < attributeCount[0]; ++i) {
				int[] arrayCount = new int[1];
				int[] type = new int[1];
				getGpuState().getGl().glGetActiveAttrib(programId, i, attributeMaxLength[0], null, 0, arrayCount, 0, type, 0, attributeNameBuffer, 0);
				int nameLength = 0;
				while (nameLength < attributeNameBuffer.length && attributeNameBuffer[nameLength] != 0) {
					++nameLength;
				}
				// check for built in attribute names (they start with "gl_")
				// we want to ignore these
				if (nameLength >= 3 &&
						attributeNameBuffer[0] == 'g' &&
						attributeNameBuffer[1] == 'l' &&
						attributeNameBuffer[2] == '_')
					continue;

				AttributeData ad = new AttributeData();
				ad.desc.name = new String(attributeNameBuffer, 0, nameLength, "UTF-8");
				ad.desc.type = type[0];
				ad.desc.arrayCount = arrayCount[0];
				ad.typeDescription = getGlslAttributeTypeDescription(type[0]);
				ad.location = getGpuState().getGl().glGetAttribLocation(programId, ad.desc.name);

				// add attribute
				attributeMap.put(ad.desc.name, i);
				attributeData[i] = ad;
			}
		} catch (GpuException e) {
			cleanup();
			throw e;
		} catch (UnsupportedEncodingException e) {
			throw new Error("UTF-8 encoding not supported");
		}
	}

	private void cleanup() {
		if (programId != 0) {
			for (int i = 0; i < ShaderType.values().length; ++i) {
				if (shaderIds[i] != 0) {
					getGpuState().getGl().glDetachShader(programId, shaderIds[i]);
				}
			}
			getGpuState().getGl().glDeleteProgram(programId);
		}
		for (int i = 0; i < ShaderType.values().length; ++i) {
			if (shaderIds[i] != 0) {
				getGpuState().getGl().glDeleteShader(shaderIds[i]);
			}
		}
	}

	protected void onRelease() {
		cleanup();
	}

	void onUnbind() {
		for (AttributeData ad : attributeData) {
			// disable attributes
			for (int t = 0; t < ad.desc.arrayCount; ++t) {
				getGpuState().getAttribute(ad.location + t).setEnabled(false);
			}
		}
	}

	private enum ShaderType {
		VERTEX_SHADER,
		GEOMETRY_SHADER,
		FRAGMENT_SHADER
	}
	private static ShaderType getShaderType(int shader) {
		switch (shader) {
			case GL3.GL_VERTEX_SHADER:
				return ShaderType.VERTEX_SHADER;
			case GL3.GL_GEOMETRY_SHADER:
				return ShaderType.GEOMETRY_SHADER;
			case GL3.GL_FRAGMENT_SHADER:
				return ShaderType.FRAGMENT_SHADER;
			default:
				throw new IllegalArgumentException("Invalid shader");
		}
	}

	private int programId;
	private int[] shaderIds = new int[ShaderType.values().length];

	// uniforms
	private static class UniformData {
		public UniformDescription desc = new UniformDescription();
		public GlslUniformTypeDescription typeDescription;
		public int locationIndex; // index of location of first element in the uniformLocations array
		public int cacheOffset;
	}
	// list of uniforms
	private UniformData[] uniformData;
	// lists of uniform locations for each uniform or uniform array
	private ArrayList<Integer> uniformLocations;
	// map of uniform names
	private HashMap<String, Integer> uniformMap;
	// the uniform cache - store float bits in ints
	private int[] uniformCache;

	// attributes
	private static class AttributeData {
		public AttributeDescription desc = new AttributeDescription();
		public GlslAttributeTypeDescription typeDescription;
		public int location;
	}
	// list of attributes
	private AttributeData[] attributeData;
	// map of attribute names
	private HashMap<String, Integer> attributeMap;
}